using System;
using System.Diagnostics;
using Community.CsharpSqlite.Entity;

namespace Community.CsharpSqlite
{
    using sqlite3_value = Mem;

    public  class AttachHelper
    {
        /*
    ** 2003 April 6
    **
    ** The author disclaims copyright to this source code.  In place of
    ** a legal notice, here is a blessing:
    **
    **    May you do good and not evil.
    **    May you find forgiveness for yourself and forgive others.
    **    May you share freely, never taking more than you give.
    **
    *************************************************************************
    ** This file contains code used to implement the ATTACH and DETACH commands.
    *************************************************************************
    **  Included in SQLite3 port to C#-SQLite;  2008 Noah B Hart
    **  C#-SQLite is an independent reimplementation of the SQLite software library
    **
    **  SQLITE_SOURCE_ID: 2010-03-09 19:31:43 4ae453ea7be69018d8c16eb8dabe05617397dc4d
    **
    **  $Header: Community.CsharpSqlite/src/attach_c.cs,v 6604176a7dbe 2010/03/12 23:35:36 Noah $
    *************************************************************************
    */
        //#include "sqliteInt.h"

#if !SQLITE_OMIT_ATTACH
        /*
** Resolve an expression that was part of an ATTACH or DETACH statement. This
** is slightly different from resolving a normal SQL expression, because simple
** identifiers are treated as strings, not possible column names or aliases.
**
** i.e. if the parser sees:
**
**     ATTACH DATABASE abc AS def
**
** it treats the two expressions as literal strings 'abc' and 'def' instead of
** looking for columns of the same name.
**
** This only applies to the root node of pExpr, so the statement:
**
**     ATTACH DATABASE abc||def AS 'db2'
**
** will fail because neither abc or def can be resolved.
*/

        private static int resolveAttachExpr(NameContext pName, Expr pExpr)
        {
            int rc = StatusCode.SQLITE_OK;
            if (pExpr != null)
            {
                if (pExpr.op != TokenKeyword.TK_ID)
                {
                    rc = sqlite3ResolveExprNames(pName, ref pExpr);
                    if (rc == StatusCode.SQLITE_OK && ExprHelper.ExprIsConstant(pExpr) == 0)
                    {
                        Utility.Sqlite3ErrorMsg(pName.pParse, "invalid name: \"%s\"", pExpr.u.zToken);
                        return StatusCode.SQLITE_ERROR;
                    }
                }
                else
                {
                    pExpr.op = TokenKeyword.TK_STRING;
                }
            }
            return rc;
        }

        /*
    ** An SQL user-function registered to do the work of an ATTACH statement. The
    ** three arguments to the function come directly from an attach statement:
    **
    **     ATTACH DATABASE x AS y KEY z
    **
    **     SELECT sqlite_attach(x, y, z)
    **
    ** If the optional "KEY z" syntax is omitted, an SQL NULL is passed as the
    ** third argument.
    */

        private static void attachFunc(
            sqlite3_context context,
            int NotUsed,
            sqlite3_value[] argv
            )
        {
            int i;
            int rc = 0;
            sqlite3 db =Sqlite3. sqlite3_context_db_handle(context);
            string zName;
            string zFile;
            Db aNew = null;
            string zErrDyn = "";

            Helper.UNUSED_PARAMETER(NotUsed);

            zFile = argv[0].z != null && (argv[0].z.Length > 0) ? Sqlite3.sqlite3_value_text(argv[0]) : "";
            zName = argv[1].z != null && (argv[1].z.Length > 0) ? Sqlite3.sqlite3_value_text(argv[1]) : "";
            //if( zFile==null ) zFile = "";
            //if ( zName == null ) zName = "";


            /* Check for the following errors:
      **
      **     * Too many attached databases,
      **     * Transaction currently open
      **     * Specified database name already being used.
      */
            if (db.nDb >= db.aLimit[LimitCategory.SQLITE_LIMIT_ATTACHED] + 2)
            {
                zErrDyn = Print.MPrintf(db, "too many attached databases - max %d",
                                         db.aLimit[LimitCategory.SQLITE_LIMIT_ATTACHED]
                    );
                goto attach_error;
            }
            if (0 == db.autoCommit)
            {
                zErrDyn = Print.MPrintf(db, "cannot ATTACH database within transaction");
                goto attach_error;
            }
            for (i = 0; i < db.nDb; i++)
            {
                string z = db.aDb[i].zName;
                Debug.Assert(z != null && zName != null);
                if (Utility.Sqlite3StrICmp(z, zName) == 0)
                {
                    zErrDyn = Print.MPrintf(db, "database %s is already in use", zName);
                    goto attach_error;
                }
            }

            /* Allocate the new entry in the db.aDb[] array and initialise the schema
      ** hash tables.
      */
            /* Allocate the new entry in the db.aDb[] array and initialise the schema
      ** hash tables.
      */
            //if( db.aDb==db.aDbStatic ){
            //  aNew = sqlite3DbMallocRaw(db, sizeof(db.aDb[0])*3 );
            //  if( aNew==0 ) return;
            //  memcpy(aNew, db.aDb, sizeof(db.aDb[0])*2);
            //}else {
            if (db.aDb.Length <= db.nDb)
                Array.Resize(ref db.aDb, db.nDb + 1);
                    //aNew = Malloc.DbRealloc(db, db.aDb, sizeof(db.aDb[0])*(db.nDb+1) );
            if (db.aDb == null) return; // if( aNew==0 ) return;
            //}
            db.aDb[db.nDb] = new Db(); //db.aDb = aNew;
            aNew = db.aDb[db.nDb]; //memset(aNew, 0, sizeof(*aNew));
            //  memset(aNew, 0, sizeof(*aNew));

            /* Open the database file. If the btree is successfully opened, use
      ** it to obtain the database schema. At this point the schema may
      ** or may not be initialised.
      */
            rc = sqlite3BtreeFactory(db, zFile, false, Const.SQLITE_DEFAULT_CACHE_SIZE,
                                     db.openFlags | FileOpenOperation.SQLITE_OPEN_MAIN_DB,
                                     ref aNew.pBt);
            db.nDb++;
            if (rc == StatusCode.SQLITE_CONSTRAINT)
            {
                rc = StatusCode.SQLITE_ERROR;
                zErrDyn = Print.MPrintf(db, "database is already attached");
            }
            else if (rc == StatusCode.SQLITE_OK)
            {
                Pager pPager;
                aNew.pSchema = Callback.SchemaGet(db, aNew.pBt);
                if (aNew.pSchema == null)
                {
                    rc = StatusCode.SQLITE_NOMEM;
                }
                else if (aNew.pSchema.file_format != 0 && aNew.pSchema.enc != Helper.ENC(db))
                {
                    zErrDyn = Print.MPrintf(db,
                                             "attached databases must use the same text encoding as main database");
                    rc = StatusCode.SQLITE_ERROR;
                }
                pPager = sqlite3BtreePager(aNew.pBt);
                sqlite3PagerLockingMode(pPager, db.dfltLockMode);
                sqlite3PagerJournalMode(pPager, db.dfltJournalMode);
                sqlite3BtreeSecureDelete(aNew.pBt,
                                         sqlite3BtreeSecureDelete(db.aDb[0].pBt, -1));
            }
            aNew.safety_level = 3;
            aNew.zName = zName; //sqlite3DbStrDup(db, zName);
            //if( rc==StatusCode.SQLITE_OK && aNew.zName==0 ){
            //  rc = StatusCode.SQLITE_NOMEM;
            //}

#if SQLITE_HAS_CODEC
if( rc==StatusCode.SQLITE_OK ){
extern int sqlite3CodecAttach(sqlite3*, int, const void*, int);
extern void sqlite3CodecGetKey(sqlite3*, int, void**, int*);
int nKey;
char *zKey;
int t = sqlite3_value_type(argv[2]);
switch( t ){
case FundamentalDataType.SQLITE_INTEGER:
case FundamentalDataType.SQLITE_FLOAT:
zErrDyn = sqlite3DbStrDup(db, "Invalid key value");
rc = StatusCode.SQLITE_ERROR;
break;

case FundamentalDataType.SQLITE_TEXT:
case FundamentalDataType.SQLITE_BLOB:
nKey = sqlite3_value_bytes(argv[2]);
zKey = (char *)sqlite3_value_blob(argv[2]);
rc = sqlite3CodecAttach(db, db.nDb-1, zKey, nKey);
break;

case FundamentalDataType.SQLITE_NULL:
/* No key specified.  Use the key from the main database */
sqlite3CodecGetKey(db, 0, (void**)&zKey, nKey);
rc = sqlite3CodecAttach(db, db.nDb-1, zKey, nKey);
break;
}
}
#endif

            /* If the file was opened successfully, read the schema for the new database.
    ** If this fails, or if opening the file failed, then close the file and
    ** remove the entry from the db.aDb[] array. i.e. put everything back the way
    ** we found it.
    */
            if (rc == StatusCode.SQLITE_OK)
            {
                sqlite3BtreeEnterAll(db);
                rc = sqlite3Init(db, ref zErrDyn);
                sqlite3BtreeLeaveAll(db);
            }
            if (rc != 0)
            {
                int iDb = db.nDb - 1;
                Debug.Assert(iDb >= 2);
                if (db.aDb[iDb].pBt != null)
                {
                    sqlite3BtreeClose(ref db.aDb[iDb].pBt);
                    db.aDb[iDb].pBt = null;
                    db.aDb[iDb].pSchema = null;
                }
                Build.ResetInternalSchema(db, 0);
                db.nDb = iDb;
                if (rc == StatusCode.SQLITE_NOMEM || rc == ExtendedResultCode.SQLITE_IOERR_NOMEM)
                {
                    ////        db.mallocFailed = 1;
                    MemPool.DbFree(db, ref zErrDyn);
                    zErrDyn = Print.MPrintf(db, "out of memory");
                }
                else if (zErrDyn == "")
                {
                    zErrDyn = Print.MPrintf(db, "unable to open database: %s", zFile);
                }
                goto attach_error;
            }

            return;

            attach_error:
            /* Return an error if we get here */
            if (zErrDyn != "")
            {
                Sqlite3.sqlite3_result_error(context, zErrDyn, -1);
                MemPool.DbFree(db, ref zErrDyn);
            }
            if (rc != 0) Sqlite3.sqlite3_result_error_code(context, rc);
        }

        /*
    ** An SQL user-function registered to do the work of an DETACH statement. The
    ** three arguments to the function come directly from a detach statement:
    **
    **     DETACH DATABASE x
    **
    **     SELECT sqlite_detach(x)
    */

        private static void detachFunc(
            sqlite3_context context,
            int NotUsed,
            sqlite3_value[] argv
            )
        {
            string zName = zName = argv[0].z != null && (argv[0].z.Length > 0) ? Sqlite3.sqlite3_value_text(argv[0]) : "";
                //(sqlite3_value_text(argv[0]);
            sqlite3 db = Sqlite3.sqlite3_context_db_handle(context);
            int i;
            Db pDb = null;
            string zErr = "";

            Helper.UNUSED_PARAMETER(NotUsed);

            if (zName == null) zName = "";
            for (i = 0; i < db.nDb; i++)
            {
                pDb = db.aDb[i];
                if (pDb.pBt == null) continue;
                if (Utility.Sqlite3StrICmp(pDb.zName, zName) == 0) break;
            }

            if (i >= db.nDb)
            {
                Print.Snprintf(200, ref zErr, "no such database: %s", zName);
                goto detach_error;
            }
            if (i < 2)
            {
                Print.Snprintf(200, ref zErr, "cannot detach database %s", zName);
                goto detach_error;
            }
            if (0 == db.autoCommit)
            {
                Print.Snprintf(200, ref zErr,
                                 "cannot DETACH database within transaction");
                goto detach_error;
            }
            if (sqlite3BtreeIsInReadTrans(pDb.pBt) || sqlite3BtreeIsInBackup(pDb.pBt))
            {
                Print.Snprintf(200, ref zErr, "database %s is locked", zName);
                goto detach_error;
            }

            sqlite3BtreeClose(ref pDb.pBt);
            pDb.pBt = null;
            pDb.pSchema = null;
            Build.ResetInternalSchema(db, 0);
            return;

            detach_error:
            Sqlite3.sqlite3_result_error(context, zErr, -1);
        }

        /*
    ** This procedure generates VDBE code for a single invocation of either the
    ** sqlite_detach() or sqlite_attach() SQL user functions.
    */

        private static void codeAttach(
            Parse pParse, /* The parser context */
            int type, /* Either ActionCode.SQLITE_ATTACH or ActionCode.SQLITE_DETACH */
            FuncDef pFunc, /* FuncDef wrapper for detachFunc() or attachFunc() */
            Expr pAuthArg, /* Expression to pass to authorization callback */
            Expr pFilename, /* Name of database file */
            Expr pDbname, /* Name of the database to use internally */
            Expr pKey /* Database key for encryption extension */
            )
        {
            int rc;
            NameContext sName;
            Vdbe v;
            sqlite3 db = pParse.db;
            int regArgs;

            sName = new NameContext(); // memset( &sName, 0, sizeof(NameContext));
            sName.pParse = pParse;

            if (
                StatusCode.SQLITE_OK != (rc = resolveAttachExpr(sName, pFilename)) ||
                StatusCode.SQLITE_OK != (rc = resolveAttachExpr(sName, pDbname)) ||
                StatusCode.SQLITE_OK != (rc = resolveAttachExpr(sName, pKey))
                )
            {
                pParse.nErr++;
                goto attach_end;
            }

#if !SQLITE_OMIT_AUTHORIZATION
if( pAuthArg ){
char *zAuthArg = pAuthArg->u.zToken;
if( UnitTest.NEVER(zAuthArg==0) ){
goto attach_end;
}
rc = sqlite3AuthCheck(pParse, type, zAuthArg, 0, 0);
if(rc!=StatusCode.SQLITE_OK ){
goto attach_end;
}
}
#endif
            //* SQLITE_OMIT_AUTHORIZATION */

            v = SelectHelper.GetVdbe(pParse);
            regArgs = ExprHelper.GetTempRange(pParse, 4);
            ExprHelper.ExprCode(pParse, pFilename, regArgs);
            ExprHelper.ExprCode(pParse, pDbname, regArgs + 1);
            ExprHelper.ExprCode(pParse, pKey, regArgs + 2);

            Debug.Assert(v != null /*|| db.mallocFailed != 0 */);
            if (v != null)
            {
                VdbeAux.VdbeAddOp3(v, OPCode.OP_Function, 0, regArgs + 3 - pFunc.nArg, regArgs + 3);
                Debug.Assert(pFunc.nArg == -1 || (pFunc.nArg & 0xff) == pFunc.nArg);
                VdbeAux.VdbeChangeP5(v, (byte) (pFunc.nArg));
                VdbeAux.VdbeChangeP4(v, -1, pFunc, P4Type.P4_FUNCDEF);

                /* Code an OPCode.OP_Expire. For an ATTACH statement, set P1 to true (expire this
        ** statement only). For DETACH, set it to false (expire all existing
        ** statements).
        */
                VdbeAux.VdbeAddOp1(v, OPCode.OP_Expire, (type == ActionCode.SQLITE_ATTACH) ? 1 : 0);
            }

            attach_end:
            ExprHelper.ExprDelete(db, ref pFilename);
            ExprHelper.ExprDelete(db, ref pDbname);
            ExprHelper.ExprDelete(db, ref pKey);
        }

        /*
    ** Called by the parser to compile a DETACH statement.
    **
    **     DETACH pDbname
    */

        public static void Detach(Parse pParse, Expr pDbname)
        {
            var detach_func = new FuncDef(
                1, /* nArg */
                Const.SQLITE_UTF8, /* iPrefEnc */
                0, /* flags */
                null, /* pUserData */
                null, /* pNext */
                detachFunc, /* xFunc */
                null, /* xStep */
                null, /* xFinalize */
                "sqlite_detach", /* zName */
                null /* pHash */
                );
            codeAttach(pParse, ActionCode.SQLITE_DETACH, detach_func, pDbname, null, null, pDbname);
        }

        /*
    ** Called by the parser to compile an ATTACH statement.
    **
    **     ATTACH p AS pDbname KEY pKey
    */

        public static void Attach(Parse pParse, Expr p, Expr pDbname, Expr pKey)
        {
            var attach_func = new FuncDef(
                3, /* nArg */
                Const.SQLITE_UTF8, /* iPrefEnc */
                0, /* flags */
                null, /* pUserData */
                null, /* pNext */
                attachFunc, /* xFunc */
                null, /* xStep */
                null, /* xFinalize */
                "sqlite_attach", /* zName */
                null /* pHash */
                );
            codeAttach(pParse, ActionCode.SQLITE_ATTACH, attach_func, p, p, pDbname, pKey);
        }
#endif
        // * SQLITE_OMIT_ATTACH */

        /*
** Initialize a DbFixer structure.  This routine must be called prior
** to passing the structure to one of the sqliteFixAAAA() routines below.
**
** The return value indicates whether or not fixation is required.  TRUE
** means we do need to fix the database references, FALSE means we do not.
*/

        public static int FixInit(
            DbFixer pFix, /* The fixer to be initialized */
            Parse pParse, /* Error messages will be written here */
            int iDb, /* This is the database that must be used */
            string zType, /* "view", "trigger", or "index" */
            Token pName /* Name of the view, trigger, or index */
            )
        {
            sqlite3 db;

            if (UnitTest.NEVER(iDb < 0) || iDb == 1) return 0;
            db = pParse.db;
            Debug.Assert(db.nDb > iDb);
            pFix.pParse = pParse;
            pFix.zDb = db.aDb[iDb].zName;
            pFix.zType = zType;
            pFix.pName = pName;
            return 1;
        }

        /*
    ** The following set of routines walk through the parse tree and assign
    ** a specific database to all table references where the database name
    ** was left unspecified in the original SQL statement.  The pFix structure
    ** must have been initialized by a prior call to AttachHelper.FixInit().
    **
    ** These routines are used to make sure that an index, trigger, or
    ** view in one database does not refer to objects in a different database.
    ** (Exception: indices, triggers, and views in the TEMP database are
    ** allowed to refer to anything.)  If a reference is explicitly made
    ** to an object in a different database, an error message is added to
    ** pParse.zErrMsg and these routines return non-zero.  If everything
    ** checks out, these routines return 0.
    */

        public static int FixSrcList(
            DbFixer pFix, /* Context of the fixation */
            SrcList pList /* The Source list to check and modify */
            )
        {
            int i;
            string zDb;
            SrcList_item pItem;

            if (UnitTest.NEVER(pList == null)) return 0;
            zDb = pFix.zDb;
            for (i = 0; i < pList.nSrc; i++)
            {
//, pItem++){
                pItem = pList.a[i];
                if (pItem.zDatabase == null)
                {
                    pItem.zDatabase = zDb; // sqlite3DbStrDup( pFix.pParse.db, zDb );
                }
                else if (Utility.Sqlite3StrICmp(pItem.zDatabase, zDb) != 0)
                {
                    Utility.Sqlite3ErrorMsg(pFix.pParse,
                                    "%s %T cannot reference objects in database %s",
                                    pFix.zType, pFix.pName, pItem.zDatabase);
                    return 1;
                }
#if !SQLITE_OMIT_VIEW || !SQLITE_OMIT_TRIGGER
                if (FixSelect(pFix, pItem.pSelect) != 0) return 1;
                if (FixExpr(pFix, pItem.pOn) != 0) return 1;
#endif
            }
            return 0;
        }

#if !SQLITE_OMIT_VIEW || !SQLITE_OMIT_TRIGGER
        public static int FixSelect(
            DbFixer pFix, /* Context of the fixation */
            Select pSelect /* The SELECT statement to be fixed to one database */
            )
        {
            while (pSelect != null)
            {
                if (FixExprList(pFix, pSelect.pEList) != 0)
                {
                    return 1;
                }
                if (FixSrcList(pFix, pSelect.pSrc) != 0)
                {
                    return 1;
                }
                if (FixExpr(pFix, pSelect.pWhere) != 0)
                {
                    return 1;
                }
                if (FixExpr(pFix, pSelect.pHaving) != 0)
                {
                    return 1;
                }
                pSelect = pSelect.pPrior;
            }
            return 0;
        }

        public static int FixExpr(
            DbFixer pFix, /* Context of the fixation */
            Expr pExpr /* The expression to be fixed to one database */
            )
        {
            while (pExpr != null)
            {
                if (Helper.ExprHasAnyProperty(pExpr, ExprFlag.EP_TokenOnly)) break;
                if (Helper.ExprHasProperty(pExpr, ExprFlag.EP_xIsSelect))
                {
                    if (FixSelect(pFix, pExpr.x.pSelect) != 0) return 1;
                }
                else
                {
                    if (FixExprList(pFix, pExpr.x.pList) != 0) return 1;
                }
                if (FixExpr(pFix, pExpr.pRight) != 0)
                {
                    return 1;
                }
                pExpr = pExpr.pLeft;
            }
            return 0;
        }

        public static int FixExprList(
            DbFixer pFix, /* Context of the fixation */
            ExprList pList /* The expression to be fixed to one database */
            )
        {
            int i;
            ExprList_item pItem;
            if (pList == null) return 0;
            for (i = 0; i < pList.nExpr; i++) //, pItem++ )
            {
                pItem = pList.a[i];
                if (FixExpr(pFix, pItem.pExpr) != 0)
                {
                    return 1;
                }
            }
            return 0;
        }
#endif

#if !SQLITE_OMIT_TRIGGER
        public static int FixTriggerStep(
            DbFixer pFix, /* Context of the fixation */
            TriggerStep pStep /* The trigger step be fixed to one database */
            )
        {
            while (pStep != null)
            {
                if (FixSelect(pFix, pStep.pSelect) != 0)
                {
                    return 1;
                }
                if (FixExpr(pFix, pStep.pWhere) != 0)
                {
                    return 1;
                }
                if (FixExprList(pFix, pStep.pExprList) != 0)
                {
                    return 1;
                }
                pStep = pStep.pNext;
            }
            return 0;
        }
#endif
    }
}